<template>
	<div class="page">
		<h1>百度 > 实时语音识别</h1>
		<h3>1. 录音</h3>
		<el-button type="danger" @click="handleAudioStart">开始录音</el-button>
		<el-button @click="handleAudioStop">停止录音</el-button>

		<br />
		<br />

		<h3>2. 打开控制台查看识别结果</h3>
	</div>
</template>

<script>
import Recorder from '@/utils/recorder'
import { pcmBlobToBase64 } from '@/utils/recorder/pcm-extend'
export default {
	name: 'Socket',
	data() {
		return {
			rec: null,
			pcmBlob: '',
			wavBlob: '',
			audioSrc: '',

			sampleBuf: '', // 样本缓冲区
			sendBuf: '', // 发送缓冲区
		}
	},
	created() {
		this.linkSocket()
	},
	methods: {
		handleAudioStart() {
			if (!this.ifSocketAlive()) {
				this.linkSocket()
			}

			// 定义录音对象 > .pcm 格式
			const rec = (this.rec = Recorder({
				type: 'pcm', // 可以定义其他格式
				bitRate: 16, // 设置比特率为 16 位
				sampleRate: 16000, // 设置采样率为 16000
				onProcess: this.recProcess, // 实时录音回调
			}))

			// 开始录音 (会判断浏览器是否存在录音权限)
			rec.open(function () {
				rec.start()
				console.log('开始录音')
			})
		},

		handleAudioStop() {
			// 释放录音资源，当然可以不释放，后面可以连续调用start
			// 但不释放时系统或浏览器会一直提示在录音，最佳操作是录完就close掉
			const close = () => {
				this.rec?.close()
				this.rec = null
			}

			this.rec?.stop(
				(blob, duration) => {
					console.log('录音结束 pcm blob:', blob)
					this.pcmBlob = blob
					close()

					Recorder.pcm2wav(
						{ sampleRate: 16000, bitRate: 16, blob: blob },
						wavBlob => {
							console.log('生成 wav:', wavBlob)
							this.wavBlob = wavBlob
							this.audioSrc = (window.URL || webkitURL).createObjectURL(wavBlob)
						},
						function (msg) {
							console.log('pcm 转 wav 失败:', msg)
						}
					)
				},
				errMsg => {
					console.log('录音失败: ' + errMsg)
					close()
				}
			)
		},

		/**
		 * 实时录音回调函数
		 *
		 * @param {Int16Array[]} buffer - 当前缓冲的PCM数据块（16位小端LE），为从开始录音到现在的所有pcm片段，每次回调可能增加0-n个不定量的pcm片段。
		 * @param {number} powerLevel - 当前缓冲的音量级别（0-100）。
		 * @param {number} bufferDuration - 已缓冲时长。
		 * @param {number} bufferSampleRate - 缓冲数据的采样率（当type支持边录边转码(Worker)时，此采样率和设置的采样率相同，否则不一定相同）。
		 * @param {number} newBufferIdx - 本次回调新增的buffer起始索引。
		 * @param {Function} asyncEnd - 异步处理完成时需要调用此回调，如果不是异步的请忽略此参数，此方法回调时必须是真异步（不能真异步时需用setTimeout包裹）。
		 * @returns {Promise<void>} - 返回一个Promise对象，表示异步操作的完成。
		 * 
		  // 返回值：onProcess如果返回true代表开启异步模式，在某些大量运算的场合异步是必须的，必须在异步处理完成时调用asyncEnd(不能真异步时需用setTimeout包裹)；
			// 返回其他值或者不返回为同步模式（需避免在回调内执行耗时逻辑）；如果开启异步模式，在onProcess执行后新增的buffer会全部替换成空数组，因此本回调开头应立即将newBufferIdx到本次回调结尾位置的buffer全部保存到另外一个数组内，处理完成后写回buffers中本次回调的结尾位置。
			// buffers=[[Int16,...],...]：缓冲的PCM数据块(16位小端LE)，为从开始录音到现在的所有pcm片段，每次回调可能增加0-n个不定量的pcm片段。
			// 注意：buffers数据的采样率为bufferSampleRate，它和set.sampleRate不一定相同，可能为浏览器提供的原始采样率rec.srcSampleRate，也可能为已转换好的采样率set.sampleRate；如需浏览器原始采样率的数据，请使用rec.buffers原始数据，而不是本回调的参数；
			// 如需明确和set.sampleRate完全相同采样率的数据，请在onProcess中自行连续调用采样率转换函数Recorder.SampleData()，配合mock方法可实现实时转码和压缩语音传输；
			// 修改或替换buffers内的数据将会改变最终生成的音频内容（注意不能改变第一维数组长度），比如简单有限的实现实时静音、降噪、混音等处理，详细参考下面的rec.buffers

			// asyncEnd：fn() 如果onProcess是异步的(返回值为true时)，处理完成时需要调用此回调，如果不是异步的请忽略此参数，此方法回调时必须是真异步（不能真异步时需用setTimeout包裹）。
			// 如果需要绘制波形之类功能，需要实现此方法即可，使用以计算好的powerLevel可以实现音量大小的直观展示，使用buffers可以达到更高级效果
			// 如果需要实时上传（发送）之类的，可以配合Recorder.SampleData方法，将buffers中的新数据连续的转换成pcm，或使用mock方法将新数据连续的转码成其他格式，可以参考文档里面的：Demo片段列表 -> 实时转码并上传-通用版；基于本功能可以做到：实时转发数据、实时保存数据、实时语音识别（ASR）等

			* 为什么要这样做？
			* 采样率转换：不同设备录制的音频采样率可能不同，将音频数据统一转换为 16000 Hz，可以确保后续处理的一致性和兼容性。
			* 数据块处理：音频数据通常以块为单位进行处理和传输，这样可以减少延迟，提高实时性。
			* 缓冲区管理：通过 sampleBuf 管理所有收集到的数据，确保每次发送的数据块大小一致，便于服务器端处理。
			* 延时控制：通过 await this.sleep(160) 控制发送数据的频率，防止一次性发送过多数据造成网络拥塞。
		 */
		async recProcess(buffer, powerLevel, bufferDuration, bufferSampleRate, newBufferIdx, asyncEnd) {
			// 从 buffer 中获取最新的一段PCM数据（即最后一个元素）。PCM数据是未压缩的音频数据
			const data_48k = buffer[buffer.length - 1]

			// 将 data_48k 封装到一个新的数组中。这样做是为了与后续处理函数的输入格式匹配
			const array_48k = new Array(data_48k)

			// 使用 Recorder.SampleData 函数将 array_48k 中的音频数据从 bufferSampleRate 采样率转换为 16000 Hz。采样率转换是为了统一处理和传输音频数据，因为不同设备可能有不同的采样率。
			const data_16k = Recorder.SampleData(array_48k, bufferSampleRate, 16000).data

			// 将转换后的 data_16k 数据添加到 sampleBuf 中。sampleBuf 是一个 Int16Array 类型的缓冲区，用于存储所有收集到的数据。
			// 使用 Int16Array.from([...this.sampleBuf, ...data_16k]) 是为了合并现有数据和新数据
			this.sampleBuf = Int16Array.from([...this.sampleBuf, ...data_16k])

			// 定义一个块大小。音频数据通常以块为单位进行处理和传输。这里每个块包含 2560 个样本（因为每个样本占 2 字节，5120 字节 / 2 字节/样本 = 2560 样本）
			const chunk_size = 5120 / 2

			// 检查 sampleBuf 是否包含足够的数据块。如果 sampleBuf 的长度大于或等于 chunk_size，则进行下一步处理
			while (this.sampleBuf.length >= chunk_size) {
				// 从 sampleBuf 中提取一个数据块
				this.sendBuf = this.sampleBuf.slice(0, chunk_size)

				// 更新 sampleBuf，移除已提取的数据块
				this.sampleBuf = this.sampleBuf.slice(chunk_size)

				// 创建一个新的 ArrayBuffer，大小为 sendBuf 的长度乘以2（因为每个样本占 2 字节）。
				const sendBufArrayBuffer = new ArrayBuffer(this.sendBuf.length * 2)

				// 使用 DataView 将 sendBuf 中的数据逐一写入 ArrayBuffer，每个样本按 16 位小端序（LE）存储。
				const view = new DataView(sendBufArrayBuffer)

				// 使用 for 循环将 sendBuf 中的数据逐一写入 view 中，按16位小端序（LE）存储
				for (let i = 0; i < this.sendBuf.length; i++) {
					view.setInt16(i * 2, this.sendBuf[i], true)
				}

				// 通过 WebSocket 发送 sendBufArrayBuffer
				if (this.ifSocketAlive()) {
					this.socket.send(sendBufArrayBuffer)
				}

				// 等待 160 毫秒，再处理下一块数据。这样可以控制数据传输的频率，防止一次性发送太多数据造成网络堵塞或处理延迟
				await this.sleep(160)
			}
		},
		sleep(ms) {
			return new Promise(resolve => setTimeout(resolve, ms))
		},

		ifSocketAlive() {
			if (!this.socket) return false
			return this.socket.readyState === WebSocket.OPEN
		},
		linkSocket() {
			// 这里的sn是百度实时语音用于排查日志，这里我图方便就用时间戳了
			let sn = new Date().getTime()

			// 1. 建立 webSocket 连接
			const socket = (this.socket = new WebSocket(`wss://vop.baidu.com/realtime_asr?sn=${sn}`))

			socket.sendMessage = param => {
				console.log('socket sendMessage', param)
				if (!param) return
				const paramStr = param ? JSON.stringify(param) : ''
				socket.send(paramStr)
			}

			socket.onopen = () => {
				console.log('Connected to the WebSocket server .')

				// 2. 发送开始帧
				this.socketStart()
			}

			socket.onmessage = event => {
				const messageStr = event.data
				const messageData = JSON.parse(messageStr)
				messageData.result && console.log(`%c${messageData.result}`, 'font-weight: bold; font-size: 20px;')
			}

			socket.onclose = () => {
				console.log('Disconnected from the WebSocket server .')
			}

			socket.onerror = error => {
				console.error('WebSocket error:', error)
			}
		},
		// 发送开始帧
		socketStart() {
			let param = {
				type: 'START',
				data: {
					appid: 69896653, // 百度实时语音识别 appid
					appkey: '5N39iuKBteVVb9eMVhFhdfhP', // 百度实时语音识别 key
					dev_pid: 15372, // 15372	中文普通话	加强标点（逗号、句号、问号、感叹号）
					cuid: 'cuid-1', // 随便填不影响使用。机器的 mac 或者其它唯一id，页面上计算 UV 用
					format: 'pcm',
					sample: 16000,
				},
			}

			this.sendMessage(param)
		},

		// 发送结束报文
		socketStop() {
			const param = {
				type: 'FINISH',
			}

			this.sendMessage(param)
			this.handleAudioStop()
		},
		sendMessage() {
			return this.socket.sendMessage(...arguments)
		},
	},
}
</script>

<style rel="stylesheet/scss" lang="scss" scoped>
.page {
	padding: 0 15px;

	.el-input {
		width: 300px;
		margin-right: 10px;
	}
}
</style>
